package com.thientan.controller

import grails.converters.JSON

import java.text.SimpleDateFormat

import org.hibernate.StaleObjectStateException
import org.hibernate.exception.ConstraintViolationException
import org.springframework.beans.BeanWrapper
import org.springframework.context.MessageSource
import org.springframework.dao.DataIntegrityViolationException
import org.springframework.dao.OptimisticLockingFailureException

import com.thientan.export.ExportHandlerImpl
import com.thientan.export.ExportType
import com.thientan.utils.Constants
import com.thientan.utils.Utility

import utils.CustomizedBeanWrapperImpl
import utils.ObjectUtils

abstract class AbstractBaseController {
	MessageSource messageSource

	def beforeInterceptor = [action:this.&debug]

	def debug() {
		log.info("${actionUri} with params ${params}")
	}

	abstract Class getDomainClass()

	abstract Object getDomainService()

	abstract String getDomainName()

	abstract String getDisplayFieldName()

	def getId(domainObj) {
		domainObj.getId()
	}
	
	String slash = File.separator
	
	protected String getRealPath() {
		servletContext.getRealPath("") + slash
	}
	
	protected String getPrintedCurrentDay() {
		new SimpleDateFormat(Constants.DEFAULT_DATE_FORMAT).format(new Date())
	}
	
	Map customInfo = [:]
	
	protected Map extractPropertiesAsMap (obj, attributes, parseDouble = null) {
		Map map = [:]
		BeanWrapper bw = new CustomizedBeanWrapperImpl(obj)
		if (parseDouble) {
			for (String attr : attributes) {
				if (attr == 'id') {
					map.put(attr, getId(obj))
				} else {
					def value = bw.getPropertyValue(attr)
					if (value instanceof Double) {
						value = Utility.moneyFormat(value)
					}
					map.put(attr.replace('.', '-'), value != null ? value : "")
				}
			}
		} else {
			for (String attr : attributes) {
				if (attr == 'id') {
					map.put(attr, getId(obj))
				} else {
					def value = bw.getPropertyValue(attr)
					map.put(attr.replace('.', '-'), value != null ? value : "")
				}
			}
		}
		map
	}
	
	protected List extractPropertiesAsMaps (objs, attributes, parseDouble = null) {
		return parseDouble ? objs.collect {extractPropertiesAsMap(it, attributes, true)} : 
							 objs.collect {extractPropertiesAsMap(it, attributes)}
	}

	protected List extractPropertiesAsMapsWithOrderNumber(objs, attributes, parseDouble = null){
		Integer stt = 1 // Begin with 1
		if (parseDouble) {
			return objs.collect { item ->
				Map mapValues = extractPropertiesAsMap(item, attributes, true)
				mapValues.putAt('stt', stt++)
				mapValues
			}
		} else {
			return objs.collect { item ->
				Map mapValues = extractPropertiesAsMap(item, attributes)
				mapValues.putAt('stt', stt++)
				mapValues
			}
		}
		
	}

	Map listAsMap(List objs) {
		[items : objs, count : objs.size()]
	}

	def getEditingObjectFromSession(objectId){
		ObjectUtils.copy(session."editingObject.${domainName}.${objectId}")
	}

	protected void putOrUpdateToSession(def object, objectId){
		session."editingObject.${domainName}.${objectId}" = object
	}
	
	protected void updateAdditionalInfoAfterSave(def object){
		// do nothing
		// currently used by hopdongcontroller to increase count phieunhan in thongkephieuthu
	}

	protected Map prepareParams(){
		// To do at the sub controller.
	}

	protected void beforeSave(editingObject){
		// To do at the sub controller.
	}

	protected Map getFormMessage(success, msgType, msgText, List errors, def type = null) {
		Map result = ["success" : success]
		Map msg = [:]
		if(msgType && msgText) {
			msg.putAt "action", msgType
			msg.putAt "msg", msgText
			if(type != null) {
				msg.putAt "type", type
			}
			result.putAt "msg", msg
		}
		if(errors) {
			result.putAt "errors", errors
		}
		result
	}

	def save = {
		String msg
		def objectId = params['id']
		def editingObject
		if (objectId) {
			editingObject = getEditingObjectFromSession(objectId)
			if (!editingObject) {
				editingObject = domainClass.get(objectId)
				editingObject.discard()
			}
		} else {
			editingObject = domainClass.newInstance()
		}
		// Binding params.
		editingObject.properties = prepareParams()
		Map returnMap = [:]
		try {
			beforeSave(editingObject)
			def savedObject = domainService.saveOrUpdate(editingObject)
			if(!editingObject.errors.allErrors){
				// Update editing object in session
				putOrUpdateToSession(savedObject, savedObject.id)
				// Update additional info when create
				updateAdditionalInfoAfterSave(savedObject)
				msg = message(code : 'default.thientan.messages.saveSuccess',
						args : [
							domainName,
							editingObject.getAt(displayFieldName)
						])
				returnMap = getFormMessage(true, "info", msg, null)
				returnMap += renderSavedDomain(editingObject)
			} else {
				log.warn('Binding/Validation errors happended during saving: ' + editingObject.errors.allErrors)
				def errors = getErrorList(editingObject.errors.allErrors, domainName)
				msg = message(code : "default.action.save.fail.who",
						args : [
							getDomainName(),
							params.getAt(displayFieldName)
						])
				returnMap = getFormMessage(false, "error", msg, errors)
			}
		} catch(OptimisticLockingFailureException ex) {
			log.error("OptimisticLockingFailureException happened during saving", ex)
			returnMap = produceConcurrentUpdateMessage(displayFieldName)
		} catch(StaleObjectStateException ex) {
			log.error("StaleObjectStateException happened during saving", ex)
			returnMap = produceConcurrentUpdateMessage(displayFieldName)
		} catch(DataIntegrityViolationException ex) {
			log.error("DataIntegrityViolationException happened during saving", ex)
			returnMap = produceConstraintViolationMessage()
		} catch(ConstraintViolationException ex) {
			log.error("ConstraintViolationException happened during saving", ex)
			returnMap = produceConstraintViolationMessage()
		} catch(Exception ex){
			log.error("Unexpected error happened during saving", ex)
			returnMap = handleSaveExceptions(ex, returnMap)
		}
		returnMap["customInfo"] = customInfo
		render (returnMap as JSON)
	}

	/**
	 * Handle exception when save domain
	 * */
	protected def handleSaveExceptions(def ex, def returnMap){
		returnMap
	}

	protected def renderSavedDomain(editingObject){
		["success" : true, id : "${editingObject.id}"]
	}

	def export() {
		try {
			// Build file name
			String timeInstance = new SimpleDateFormat(Constants.DATETIME_EXPORT).format(Calendar.instance.time)
			String fileName = exportFileName + additionalExportName + timeInstance + exportType.extension

			// Set base path to store the exported file
			String basePath = "${realPath}exported${slash}"

			Map exportParams = [
						"requestParams" : params,
						"fileName" : fileName,
						"basePath" : basePath,
						"exportType" : getExportType()
					]
			Map exportData = buildExportData(exportParams)
			String resourcePath = "${realPath}templates${slash}${exportFileName}.jrxml"
			domainService.export(exportData, exportParams['fileName'],
					exportParams['basePath'], exportParams['exportType'], null, resourcePath)
			// return exported file to client
			def exportHandlers= new ExportHandlerImpl()
			exportHandlers.sendFile(response, basePath + fileName, fileName)
		} catch(Exception e) {
			log.error("An error occurred when exporting file: ", e)
		}
	}
	
	protected Map buildExportData(Map exportParams){
	}

	protected ExportType getExportType(){
		ExportType.XLS
	}

	protected String getExportFileName(){
		"export"
	}
	
	protected String getAdditionalExportName() {
		""
	}

	/**
	 * Produce warning message in the case of the concurrent update exception.
	 *
	 * @param displayName the property of the domain object used to display in message
	 * @param returnMap Map returned to client.
	 * */
	protected Map produceConcurrentUpdateMessage(String displayName){
		String msg = message(code : "default.constraint.optimistic.locking.which",
				args : [
					message(code : "default.text.save"),
					params.getAt(displayName) ?: displayName
				])
		getFormMessage(false, msg, null, OptimisticLockingFailureException.class.name)
	}

	/**
	 * Produce warning message in the case of the constraint violation exception.
	 *
	 * @param displayName the property of the domain object used to display in message
	 * @param returnMap Map returned to client.
	 * */
	protected Map produceConstraintViolationMessage(){
		String msg = message(code : "default.constraint.violations")
		getFormMessage(false, msg, null, DataIntegrityViolationException.class.name)
	}

	/**
	 * Process a domain's constraint errors. Make a list of messages for each field
	 * error.
	 * @param errors - list of constraint errors
	 * @param prefix - domain string (optional)
	 * return
	 */
	protected List getErrorList(def errors, String prefix) {
		List result = []
		List args = null                   // validation values
		String domain = null               // domain object
		String field = null                // domain field
		def constraint = null              // validated constraint
		String value = null                // field value
		String start = null                // starting range
		String end = null                  // ending range
		boolean bWhich = org.springframework.util.StringUtils.hasText(prefix)
		String msg

		errors.each {
			args = it.arguments
			field = it.field
			constraint = it.code

			// Violation error on a Java object type
			if(constraint == 'typeMismatch' || constraint == 'methodInvocation') {
				args = it.codes
				domain = args.get(0)
				domain = domain.substring(0, domain.size()-field.size()-1) // strip off field
				constraint = args.get(2)                                   // full constraint with Java type
				value = ""
				start = ""
				end = ""
			} else {
				domain = args.get(1)
				try {
					value = args.get(2)
				} catch(IndexOutOfBoundsException ex) {
					value = ""
				}

				try {
					start = args.get(3)
				} catch(IndexOutOfBoundsException ex) {
					start = ""
				}

				try {
					end = args.get(4)
				} catch(IndexOutOfBoundsException ex) {
					end = ""
				}
			}
			msg = getConstraintMessage(domain, field, constraint, value, start, end, bWhich)
			result.add([id : field, action : 'error', msg :  msg])
		}
		result
	}

	/**
	 * Get default messages from the standard oxyc-web-core message.properties file
	 * where standard constraint messages match the pattern:
	 *     default.constraint.<constraint>.invalid.<which>
	 * Domain field constraints are handled separately by domain and field with a message
	 * of the form:
	 *     default.<domain>.<field>.validator.invalid.<which>
	 * Lookup a domain specific message first, if one is not found then lookup a generic
	 * constraint message. The message lookup expects a list of ordered values:
	 *  {0} - Domain
	 *  {1} - Domain field
	 *  {2} - Value of field
	 *  {3} - Start value of range
	 *  {4} - End value of range
	 * @param domain
	 * @param field
	 * @param constraint - variable object type (String and DefaultMessageSourceResolvable)
	 * @param value
	 * @param start
	 * @param end
	 * @param bWhich - boolean, true = apply Domain and Field to message
	 * @return
	 */
	protected String getConstraintMessage(String domain, String field, def constraint, String value, String start,
	String end, boolean bWhich) {
		String lookup
		String msg
		List list = []
		String sDomain
		String sField
		int i

		// Domain
		try {
			i = domain.lastIndexOf(".")                                     // Remove package from domain name
			domain = domain.substring(i + 1).toLowerCase()
			sDomain = message(code: "default.text.${domain}", args: null)   // Resource string value
		} catch(org.springframework.context.NoSuchMessageException ex) {
			log.error("Unable to find message: " + domain, ex)
			sDomain = domain
		}

		// Domain field
		try {
			field = field.toLowerCase()
			sField = message(code: "default.text.${field}", args: null)     // Resource string value
		} catch(org.springframework.context.NoSuchMessageException ex) {
			log.error("Unable to find message: " + field, ex)
			sField = field
		}
		sField = sField.toLowerCase()

		// Constraint type
		if(constraint instanceof String) {
			if(! constraint.contains("typeMismatch")) {
				i = constraint.indexOf(".")
				if(i > -1) {
					constraint = constraint.substring(0, i)                 // constraint only, remove why
				}
			}
			constraint = constraint.toLowerCase()
		} else {
			constraint = "unknown"                                          // unknown constraint, use default message
		}

		// Values for message
		list = [
			sDomain,
			sField,
			value,
			start,
			end
		]

		try {
			lookup = "default.${domain}.${field}.${constraint}.invalid" + (bWhich ? ".which" : "")
			msg =  message(code: lookup, args: list)
		} catch(org.springframework.context.NoSuchMessageException e){
			// do nothing
		}

		if(msg == lookup) {
			try {
				lookup = "default.constraint.${constraint}.invalid" + (bWhich ? ".which" : "")
				msg =  message(code: lookup, args: list)
			} catch(org.springframework.context.NoSuchMessageException ex){
				log.error("Unable to find message: " + lookup, ex)
			}
		}

		if(msg == lookup) {
			try {
				msg = message(code: "default.field.invalid", args: null)
			} catch(org.springframework.context.NoSuchMessageException ex){
				log.error("Unable to find message: " + lookup)
			}
		}

		msg
	}
}
